In [1]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
In [2]:
dr14 = pd.read_csv('Dataset/SDSS_DR14.csv')
dr16 = pd.read_csv('Dataset/SDSS_DR16.csv')
dr17 = pd.read_csv('Dataset/SDSS_DR17.csv')
dr18 = pd.read_csv('Dataset/SDSS_DR18.csv') 
dataframe = pd.concat([dr14, dr16, dr17, dr18], ignore_index=True, sort=False)
In [3]:
df = dataframe.dropna(axis=1)
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 310000 entries, 0 to 309999
Data columns (total 18 columns):
 #   Column     Non-Null Count   Dtype  
---  ------     --------------   -----  
 0   objid      310000 non-null  object 
 1   ra         310000 non-null  float64
 2   dec        310000 non-null  float64
 3   u          310000 non-null  float64
 4   g          310000 non-null  float64
 5   r          310000 non-null  float64
 6   i          310000 non-null  float64
 7   z          310000 non-null  float64
 8   run        310000 non-null  int64  
 9   rerun      310000 non-null  int64  
 10  camcol     310000 non-null  int64  
 11  field      310000 non-null  int64  
 12  specobjid  310000 non-null  object 
 13  class      310000 non-null  object 
 14  redshift   310000 non-null  float64
 15  plate      310000 non-null  int64  
 16  mjd        310000 non-null  int64  
 17  fiberid    310000 non-null  int64  
dtypes: float64(8), int64(7), object(3)
memory usage: 42.6+ MB

Usuwamy niepotrzebne dane takie jak ID obserwacji i inne techniczne informacje My będziemy korzystać z:

  • u = Dane z filtra ultrafioletu
  • g = Dane z filtra zielonego
  • r = Dane z filtra czerwonego
  • i = Dane z filtra bliskiej podczerwi (Near Infrared)
  • z = Dane z filtra podczerwieni
  • redshift = przesunięcie ku czerwieni na podstawie zwiększenia długości fali

Jasności na filtrach są podawane w Asinh magnitudo. Wyraża się ono wzorem:

\begin{aligned} \frac{-2.5}{\ln 10} * \text{arcsinh}( \frac{\frac{f}{f_0}}{2b} +\ln(b)) \end{aligned}

gdzie:

  • b jest stałą zmiękczającą można sprawdzić pod linkiem.
  • f$_0$ to jasność odniesienia. Klasycznie jest to jasność gwiazdy Vega.
  • f to obserwowana jasność
In [4]:
df=df.drop(columns=['objid','run','rerun','camcol','field','specobjid','plate','mjd','fiberid'])

Dodatkowo pozbędziemy się fałszywych danych poza zakresami

In [5]:
df.describe()
Out[5]:
ra dec u g r i z redshift
count 310000.000000 310000.000000 310000.000000 310000.000000 310000.000000 310000.000000 310000.000000 310000.000000
mean 174.694084 22.097973 19.716598 18.413805 17.770995 17.382543 17.075167 0.300031
std 85.593731 22.738541 18.124083 18.109926 1.907456 18.081449 36.024479 0.579806
min 0.003092 -19.495456 -9999.000000 -9999.000000 9.005167 -9999.000000 -9999.000000 -0.009971
25% 131.354822 1.042457 18.510617 17.143935 16.472307 16.136660 15.908515 0.000107
50% 175.860912 17.145758 19.184505 17.904820 17.357705 17.063675 16.893270 0.070177
75% 223.633481 41.176901 20.162707 19.213520 18.882188 18.631750 18.436143 0.279405
max 359.999810 84.490494 32.781390 31.602240 31.990100 32.141470 29.383740 7.011245
In [6]:
df['class'].describe()
Out[6]:
count     310000
unique         3
top       GALAXY
freq      168109
Name: class, dtype: object
In [7]:
for column in ['ra','dec','class']:  # Ensure numerical data
    plt.figure()  # Create a new figure for each histogram
    sns.histplot(df[column], kde=True, bins=100, color='blue')  # Histogram with KDE
    plt.title(f'Histogram for {column}')
    plt.xlabel(column)
    plt.ylabel('Amount')
    plt.grid(axis='y', alpha=0.75)
    plt.show()

plt.figure()  # Create a new figure for each histogram
for column in df.select_dtypes(include=['number']).columns: 
    if column != 'ra'and column!='dec' and column != 'class':
        sns.histplot(data=df,x=column,label=column,multiple='stack', kde=False, bins=10)  # Histogram with KDE
plt.title(f'Histogram for our data')
plt.legend()
plt.xlabel('value')
plt.ylabel('amount')
plt.grid(axis='y', alpha=0.75)
plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Wybieramy zakresy dla poszczególnych kolumn a następnie filtrujemy dane - ryzykujemy utratę niektórych przypadków, ale ewidentnie w zakresie danych są takie które są fizycznie niemożliwe:

In [8]:
ranges={
"u": (-2000,50),
"g": (-2000,50),
"r": (0,50),
"z": (-2000,50),
}
for column, (min_val, max_val) in ranges.items():
    df = df[(df[column] >= min_val) & (df[column] <= max_val)]
df = df.reset_index(drop=True)

plt.figure()  # Create a new figure for each histogram
for column in df.select_dtypes(include=['number']).columns: 
    if column != 'ra'and column!='dec' and column != 'class':
        sns.histplot(data=df,x=column,label=column,multiple='stack', kde=False, bins=10)  # Histogram with KDE
plt.title(f'Histogram for our data')
plt.legend()
plt.xlabel('value')
plt.ylabel('amount')
plt.grid(axis='y', alpha=0.75)
plt.show()
No description has been provided for this image

Podzielmy dane na dwa oddzielne zestawy:

In [9]:
y=df['class']
x=df.drop('class',axis=1)

Przeprowadzimy teraz

Analizę składowych głównych¶

zaczniemy od standaryzacji zmiennych

In [10]:
numeric_df = x.select_dtypes(include=[np.number])
scaler = StandardScaler()
scaled_data = scaler.fit_transform(numeric_df)

Teraz przejdziemy do właściwej analizy sprawdzimy dwie rzeczy:

  • jak liczba składowych wpływa na całkowitą wariancję
  • następnie wybierzemy jedną konkretną liczbę składowych głownych i obliczymy składowe główne dla niej
In [11]:
# Tworzenie obiektu PCA, gdzie nie określasz liczby komponentów
pca = PCA()

# Dopasowanie i transformacja danych
pca.fit(scaled_data)

# Sprawdzenie skumulowanej wariancji wyjaśnianej przez kolejne komponenty
cumulative_variance = np.cumsum(pca.explained_variance_ratio_)
print(f"Skumulowana wariancja: {cumulative_variance}")

# Wykres skumulowanej wariancji
plt.figure(figsize=(8,5))
plt.plot(cumulative_variance)
plt.xlabel('Liczba głównych składowych')
plt.ylabel('Skumulowana wyjaśniona wariancja')
plt.title('Wykres skumulowanej wyjaśnionej wariancji')
plt.grid(True)
plt.show()
Skumulowana wariancja: [0.61301954 0.74765284 0.86279967 0.95484645 0.99312401 0.99754811
 0.99902642 1.        ]
No description has been provided for this image
In [12]:
# Utworzenie obiektu PCA 
n_components=4 #liczba składowych głównych
pca = PCA(n_components)

# Dopasowanie i transformacja danych
principal_components = pca.fit_transform(scaled_data)

# Sprawdzenie ile wariancji wyjaśniają komponenty
explained_variance = pca.explained_variance_ratio_
print(f"Wariancja wyjaśniana przez każdą główną składową: {explained_variance}")

# Utworzenie DataFrame z wynikami analizy PCA (ze zredukowanymi wymiarami)
pca_df = pd.DataFrame(data=principal_components, columns=[f'PC{i+1}' for i in range(n_components)])

# Uzyskanie macierzy składowych
components = pca.components_

# Tworzenie DataFrame z wynikami
components_df = pd.DataFrame(components, columns=x.select_dtypes(include=[np.number]).columns, index=[f'PC{i+1}' for i in range(components.shape[0])])

# Wyświetlenie macierzy składowych
print(components_df)
Wariancja wyjaśniana przez każdą główną składową: [0.61301954 0.1346333  0.11514683 0.09204678]
           ra       dec         u         g         r         i         z  \
PC1  0.009132  0.029960  0.393228  0.437499  0.446836  0.440635  0.430594   
PC2  0.712033  0.701017 -0.016387 -0.014054 -0.015194 -0.015238 -0.015914   
PC3 -0.700426  0.707398 -0.050002 -0.026158 -0.011492 -0.002054  0.004495   
PC4  0.045491 -0.085055 -0.357515 -0.198924 -0.076150  0.001872  0.041001   

     redshift  
PC1  0.271574  
PC2  0.020044  
PC3  0.075169  
PC4  0.903230  

Widzimy, że wartość dla ra i dec w pierwszej składowej są bardzo małe, natomiast składowe które je wyjaśniają (2 i 3) bardzo słabo wyjaśniają pozostałe. Sprawdźmy na wszelki wypadek macierz korelacji, bo być może ra i dec są wgl niepotrzebne.

In [13]:
plt.figure(figsize=(10,6))
correlation = pd.concat([x,y],axis=1)
sns.heatmap(x.corr(), annot=True,cmap='jet',fmt='.2f') #jet,copper, coolwarm
plt.title('Correlation matrix')
plt.show()
No description has been provided for this image

Wartości ra i dec praktycznie wgl nie pomagają nam w klasyfikacji, pozbędziemy się ich i jeszcz raz ustalimy składowe główne.

In [14]:
x=x.loc[:, x.columns != 'ra']
x=x.loc[:, x.columns != 'dec']
numeric_df = x.select_dtypes(include=[np.number])
scaler = StandardScaler()
scaled_data = scaler.fit_transform(numeric_df)
# Tworzenie obiektu PCA, gdzie nie określasz liczby komponentów
pca = PCA()

# Dopasowanie i transformacja danych
pca.fit(scaled_data)

# Sprawdzenie skumulowanej wariancji wyjaśnianej przez kolejne komponenty
cumulative_variance = np.cumsum(pca.explained_variance_ratio_)
print(f"Skumulowana wariancja: {cumulative_variance}")

# Wykres skumulowanej wariancji
plt.figure(figsize=(8,5))
plt.plot(cumulative_variance)
plt.xlabel('Liczba głównych składowych')
plt.ylabel('Skumulowana wyjaśniona wariancja')
plt.title('Wykres skumulowanej wyjaśnionej wariancji')
plt.grid(True)
plt.show()
Skumulowana wariancja: [0.81672772 0.93976303 0.99083011 0.99673048 0.99870185 1.        ]
No description has been provided for this image
In [15]:
# Utworzenie obiektu PCA 
n_components=2 #liczba składowych głównych
pca = PCA(n_components)

# Dopasowanie i transformacja danych
principal_components = pca.fit_transform(scaled_data)

# Sprawdzenie ile wariancji wyjaśniają komponenty
explained_variance = pca.explained_variance_ratio_
print(f"Wariancja wyjaśniana przez każdą główną składową: {explained_variance}")

# Utworzenie DataFrame z wynikami analizy PCA (ze zredukowanymi wymiarami)
pca_df = pd.DataFrame(data=principal_components, columns=[f'PC{i+1}' for i in range(n_components)])

# Uzyskanie macierzy składowych
components = pca.components_

# Tworzenie DataFrame z wynikami
components_df = pd.DataFrame(components, columns=x.select_dtypes(include=[np.number]).columns, index=[f'PC{i+1}' for i in range(components.shape[0])])

# Wyświetlenie macierzy składowych
print(components_df)
Wariancja wyjaśniana przez każdą główną składową: [0.81672772 0.12303531]
            u         g         r         i         z  redshift
PC1  0.393470  0.437721  0.447063  0.440858  0.430811  0.271593
PC2 -0.361179 -0.200761 -0.077152  0.001429  0.041021  0.906426
In [16]:
PCA_values =   x.values @ components_df.T.values
df_PCA_x = pd.DataFrame(PCA_values,columns=[f'PC{i+1}' for i in range(components.shape[0])])
#df_PCA=pd.concat([df_PCA_x,y],axis=1)
df_PCA = df_PCA_x
df_PCA['class'] = pd.DataFrame(y)
df_PCA
df_PCA
Out[16]:
PC1 PC2 class
0 35.645608 -11.038692 STAR
1 36.664800 -10.787298 STAR
2 38.206279 -11.175851 GALAXY
3 35.380231 -10.321350 STAR
4 35.829161 -10.166418 STAR
... ... ... ...
309991 39.278134 -11.324122 STAR
309992 38.765651 -11.136801 STAR
309993 36.659873 -10.908642 GALAXY
309994 35.120367 -10.100886 STAR
309995 36.016473 -10.425274 GALAXY

309996 rows × 3 columns

In [17]:
sns.scatterplot(x='PC1',y='PC2',hue='class',data=df_PCA)
Out[17]:
<Axes: xlabel='PC1', ylabel='PC2'>
No description has been provided for this image

Alternatywnie moglibyśmy również zmaksymalizować dokładność poprzez dodanie jeszcze jednej składowej

In [18]:
# Utworzenie obiektu PCA 
n_components=3 #liczba składowych głównych
pca = PCA(n_components)

# Dopasowanie i transformacja danych
principal_components = pca.fit_transform(scaled_data)

# Sprawdzenie ile wariancji wyjaśniają komponenty
explained_variance = pca.explained_variance_ratio_
print(f"Wariancja wyjaśniana przez każdą główną składową: {explained_variance}")

# Utworzenie DataFrame z wynikami analizy PCA (ze zredukowanymi wymiarami)
pca_df = pd.DataFrame(data=principal_components, columns=[f'PC{i+1}' for i in range(n_components)])

# Uzyskanie macierzy składowych
components = pca.components_

# Tworzenie DataFrame z wynikami
components_df = pd.DataFrame(components, columns=x.select_dtypes(include=[np.number]).columns, index=[f'PC{i+1}' for i in range(components.shape[0])])

# Wyświetlenie macierzy składowych
print(components_df)
Wariancja wyjaśniana przez każdą główną składową: [0.81672772 0.12303531 0.05106709]
            u         g         r         i         z  redshift
PC1  0.393470  0.437721  0.447063  0.440858  0.430811  0.271593
PC2 -0.361179 -0.200761 -0.077152  0.001429  0.041021  0.906426
PC3  0.665579  0.212693 -0.146436 -0.363845 -0.503462  0.323212
In [19]:
PCA_values =   x.values @ components_df.T.values
df_PCA_x = pd.DataFrame(PCA_values,columns=[f'PC{i+1}' for i in range(components.shape[0])])
#df_PCA=pd.concat([df_PCA_x,y],axis=1)
df_PCA_alternative = df_PCA_x
df_PCA_alternative['class'] = pd.DataFrame(y)
df_PCA_alternative
Out[19]:
PC1 PC2 PC3 class
0 35.645608 -11.038692 0.944882 STAR
1 36.664800 -10.787298 -0.611106 STAR
2 38.206279 -11.175851 -0.424870 GALAXY
3 35.380231 -10.321350 -0.833439 STAR
4 35.829161 -10.166418 -1.654420 STAR
... ... ... ... ...
309991 39.278134 -11.324122 -1.299621 STAR
309992 38.765651 -11.136801 -1.396056 STAR
309993 36.659873 -10.908642 0.104838 GALAXY
309994 35.120367 -10.100886 -1.231751 STAR
309995 36.016473 -10.425274 -1.029245 GALAXY

309996 rows × 4 columns

Wyświetlimy wykres interaktywnie, w celu poprawy wydajności pobierzemy więc próbkę naszych punktów (~1/3 całości)

In [20]:
import plotly.express as px
df_PCA_alternative_sample=df_PCA_alternative.sample(100000, random_state=32)
fig=px.scatter_3d(df_PCA_alternative_sample,x='PC1',y='PC2',z='PC3',color='class' )
fig.update_traces(marker={'size': 1})
fig.show()

Dodanie dodatkowego wymiaru uwypukla poprzednio już widoczną różnicę między QSO a innymi obiektami. Dodatkowo nieco wyraźniejsza staje się różnica między gwiazdami i galaktymi - te drugie przyjmują bliższe są ujemnych wartości dla PC3.

Klasteryzacja

KMeans

Normalizacja danych - przygotowanie do klasteryzacji

In [21]:
pd.set_option('future.no_silent_downcasting', True)
uniques = pd.unique(df['class'])
binary_map = {}
for number, value in enumerate(uniques):
    binary_map[value] = number
df['class'] = df['class'].replace(binary_map)
binary_map
Out[21]:
{'STAR': 0, 'GALAXY': 1, 'QSO': 2}
In [22]:
df_class = df['class']
df = df.drop('class',axis=1)
scaler = StandardScaler()
data_scaled = scaler.fit_transform(df)
df['class'] = df_class

Przed normalizacją usuneliśmy z danych klase obiektu żeby nie mała ona wpływu na dopasowanie do klasteru

Klasteryzacja z użyciem KMeans

In [23]:
kmeans = KMeans(n_clusters=3, random_state=42)
clusters = kmeans.fit_predict(data_scaled)
df['Cluster'] = clusters

Metoda łokcia

Metoda łokcia to technika stosowana w analizie skupień (clustering) w celu określenia optymalnej liczby klastrów w algorytmie grupowania, takim jak k-means

In [24]:
inertias = []
for k in range(1, 11):
    kmeans_ml = KMeans(n_clusters=k, random_state=42)
    kmeans_ml.fit(data_scaled)
    inertias.append(kmeans_ml.inertia_)

plt.plot(range(1, 11), inertias, marker='o')
plt.title("Metoda Łokcia")
plt.xlabel("Liczba klastrów")
plt.ylabel("Inercja")
plt.show()
No description has been provided for this image

W naszym przypadku zdecydowaliśmy się na 3 klastry

Rozkład danych

In [25]:
df.groupby('Cluster').mean()
Out[25]:
ra dec u g r i z redshift class
Cluster
0 176.872015 24.617398 22.842321 21.575674 20.598721 19.990958 19.663840 0.892420 1.196258
1 170.730654 19.173954 18.199397 16.663351 15.988618 15.676159 15.462196 0.044983 0.618286
2 176.855472 23.161519 19.287995 18.165989 17.666329 17.418949 17.279495 0.175697 0.740211

W powyższej tabeli widzimy srednią wartość poszczególnych kolumn dla każdego z klastrów Poniżej znajduje liczba elementów przypadających na dany klaster i klase

In [26]:
df['Cluster'].value_counts()
Out[26]:
Cluster
2    126641
1    109590
0     73765
Name: count, dtype: int64
In [27]:
df['class'].value_counts()
Out[27]:
class
1    168107
0    101072
2     40817
Name: count, dtype: int64
In [28]:
plt.figure(figsize=(6, 5))
sns.heatmap(pd.crosstab(df['class'], df['Cluster']), annot=True, linewidths=0.5, fmt='d')
plt.title("Contingency Table Heatmap: Class vs Cluster")
plt.ylabel("Class")
plt.xlabel("Cluster")
plt.show()
No description has been provided for this image

Reprezentacja ilości elementów danej klasy w danym klastrze

In [29]:
contingency_table = pd.crosstab(df['class'], df['Cluster'])
contingency_percentage =  contingency_table.div(contingency_table.sum(axis=1), axis=0) * 100
plt.figure(figsize=(6, 5))
sns.heatmap(contingency_percentage, annot=True, linewidths=0.5, fmt='f')
plt.title("Contingency Table Heatmap: Class vs Cluster")
plt.ylabel("Class")
plt.xlabel("Cluster")
plt.show()
No description has been provided for this image

Reprezentacja rozkładu elementów klas na klastry

Poniżej znajduje sie kilka wykresów przedstawiających wizualizacje podziału

In [30]:
feature_1 = df['z']
feature_2 = df['g']
feature_3 = df['u']
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
scatter = ax.scatter(feature_1, feature_2, feature_3, c=clusters, cmap='viridis', s=1, alpha=0.2)
ax.set_title("Wizualizacja klasteryzacji w 3D")
ax.set_xlabel("Cecha 1")
ax.set_ylabel("Cecha 2")
ax.set_zlabel("Cecha 3")
ax.view_init(elev=40, azim=240)
ax.grid(True)
plt.show()
No description has been provided for this image
In [31]:
feature_1 = df['ra']
feature_2 = df['dec']
feature_3 = df['u']
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
scatter = ax.scatter(feature_1, feature_2, feature_3, c=clusters, cmap='viridis', s=1, alpha=0.2)
ax.set_title("Wizualizacja klasteryzacji w 3D")
ax.set_xlabel("Cecha 1")
ax.set_ylabel("Cecha 2")
ax.set_zlabel("Cecha 3")
ax.view_init(elev=40, azim=240)
ax.grid(True)
plt.show()
No description has been provided for this image
In [32]:
feature_1 = df['redshift']
feature_2 = df['class']
feature_3 = df['u']
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
scatter = ax.scatter(feature_1, feature_2, feature_3, c=clusters, cmap='viridis', s=1, alpha=0.2)
ax.set_title("Wizualizacja klasteryzacji w 3D")
ax.set_xlabel("Cecha 1")
ax.set_ylabel("Cecha 2")
ax.set_zlabel("Cecha 3")
ax.view_init(elev=40, azim=240)
ax.grid(True)
plt.show()
No description has been provided for this image

KMeans na PCA

Dla prorównania przeprowadzamy klsateryzacje na danych które zostały poddane redukcji wymiarów

In [33]:
uniques = pd.unique(df_PCA['class'])
binary_map = {}
for number, value in enumerate(uniques):
    binary_map[value] = number
df_PCA['class'] = df_PCA['class'].replace(binary_map)
binary_map
Out[33]:
{'STAR': 0, 'GALAXY': 1, 'QSO': 2}
In [34]:
scaler_pca = StandardScaler()
df_PCA_class = df_PCA['class']
df_PCA = df_PCA.drop('class',axis=1)
data_scaled_pca = scaler.fit_transform(df_PCA)
df_PCA['class'] = df_PCA_class
kmeans_pca = KMeans(n_clusters=3, random_state=42)
clusters_pca = kmeans_pca.fit_predict(data_scaled_pca)
df_PCA['Cluster'] = clusters_pca
In [35]:
inertias = []
for k in range(1, 11):
    kmeans_ml = KMeans(n_clusters=k, random_state=42)
    kmeans_ml.fit(data_scaled_pca)
    inertias.append(kmeans_ml.inertia_)

plt.plot(range(1, 11), inertias, marker='o')
plt.title("Metoda Łokcia")
plt.xlabel("Liczba klastrów")
plt.ylabel("Inercja")
plt.show()
No description has been provided for this image
In [36]:
df_PCA.groupby('Cluster').mean()
Out[36]:
PC1 PC2 class
Cluster
0 45.512034 -13.098932 0.974886
1 38.619602 -10.988799 0.854203
2 34.834026 -10.239338 0.587558
In [37]:
df_PCA['Cluster'].value_counts()
Out[37]:
Cluster
1    163927
2     84390
0     61679
Name: count, dtype: int64
In [38]:
df_PCA['class'].value_counts()
Out[38]:
class
1    168107
0    101072
2     40817
Name: count, dtype: int64
In [39]:
plt.figure(figsize=(6, 5))
sns.heatmap(pd.crosstab(df_PCA['class'], df_PCA['Cluster']), annot=True, linewidths=0.5, fmt='d')
plt.title("Contingency Table Heatmap: Class vs Cluster")
plt.ylabel("Class")
plt.xlabel("Cluster")
plt.show()
No description has been provided for this image
In [40]:
contingency_table = pd.crosstab(df_PCA['class'], df_PCA['Cluster'])
contingency_percentage =  contingency_table.div(contingency_table.sum(axis=1), axis=0) * 100
plt.figure(figsize=(6, 5))
sns.heatmap(contingency_percentage, annot=True, linewidths=0.5, fmt='f')
plt.title("Contingency Table Heatmap: Class vs Cluster")
plt.ylabel("Class")
plt.xlabel("Cluster")
plt.show()
No description has been provided for this image
In [41]:
feature_1 = df_PCA['PC1']
feature_2 = df_PCA['PC2']
feature_3 = df_PCA['class'].astype(float)
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
scatter = ax.scatter(feature_1, feature_2, feature_3, c=clusters, cmap='viridis', s=1, alpha=0.2)
ax.set_title("Wizualizacja klasteryzacji w 3D")
ax.set_xlabel("Cecha 1")
ax.set_ylabel("Cecha 2")
ax.set_zlabel("Cecha 3")
ax.view_init(elev=40, azim=240)
ax.grid(True)
plt.show()
No description has been provided for this image

Analiza koszykowa/reguły asocjacyjne

odnawiamy opis klas z liczb na słowa

In [42]:
df['class'] = pd.DataFrame(y)

Spójrzmy na zakresy w jakich znajdują się nasze dane, aby uzyskać intuicyjne pojęcie na temat tego, czego będziemy szukać.

In [43]:
def calculate_ranges(df, features, target):
    rules = {}
    for cls in df[target].unique():
        class_data = df[df[target] == cls]
        rules[cls] = {}
        for feature in features:
            rules[cls][feature] = {
                "min": class_data[feature].min(),
                "max": class_data[feature].max()
            }
    return rules

features = ["u", "g", "r","z","i","redshift"]
rules = calculate_ranges(df, features, target="class")

for cls, cls_rules in rules.items():
    print(f"Zakresy dla klasy '{cls}':")
    for feature, bounds in cls_rules.items():
        print(f"  {feature}: {bounds['min']} <= x <= {bounds['max']}")
Zakresy dla klasy 'STAR':
  u: 10.61181 <= x <= 30.66039
  g: 9.668339 <= x <= 30.607
  r: 9.005167 <= x <= 31.69816
  z: 8.947795 <= x <= 28.6687
  i: 8.848403 <= x <= 30.98087
  redshift: -0.004267853 <= x <= 0.004563184
Zakresy dla klasy 'GALAXY':
  u: 11.72647 <= x <= 29.32565
  g: 11.85493 <= x <= 31.60224
  r: 10.8467 <= x <= 31.9901
  z: 10.42036 <= x <= 29.38374
  i: 10.5178 <= x <= 32.10178
  redshift: -0.009970667 <= x <= 1.995524
Zakresy dla klasy 'QSO':
  u: 10.99623 <= x <= 32.78139
  g: 10.70823 <= x <= 27.89482
  r: 9.801497 <= x <= 28.37412
  z: 10.06144 <= x <= 28.79055
  i: 9.557886 <= x <= 32.14147
  redshift: 0.000460623 <= x <= 7.011245

Aby przeprowadzić analizę koszykową musimy przejść ze zmiennych ciągłych na dyskretne. Wyznaczymy odpowiednie zakresy. Jak wspomniano poprzednio wartości u, g, r, z, i to zasadniczo jasności w różnych spektrach światła, dlatego podzielimy je na identyczne zakresy:

  • $\infty > x > 22$, blade (dim)
  • $20 >= x > 17$, średnie (medium)
  • $17 >= x > - \infty $, jasne (bright)

W przypadku przesunięcia ku czerwieni wybierzemy wartości:

  • $x<0.05 $, niskie
  • $0.05<=x<1.5$, średnie
  • $ 2<=x$, wysokie

Wybór zakresów jest orientacyjny, ich większa ilość zapewne zwiększyłaby dokładność.

In [44]:
x_copy = x.copy()
medium_upperbound = 22
bright_upperbound = 17
low_redshift_upperbound = 0.05
medium_redshift_upperbound = 1.5

for column in x_copy:   
    if column!="redshift":
        name = "dim_"+column
        x_copy[name] = x_copy[column] > medium_upperbound
        name = "medium_"+column
        x_copy[name] = (x_copy[column] <= medium_upperbound) &   (x_copy[column] > bright_upperbound)
        name= "bright_"+column
        x_copy[name] =x_copy[column] <= bright_upperbound
    else:
        name="low_"+column
        x_copy[name]=x_copy[column]<low_redshift_upperbound
        name="medium_"+column
        x_copy[name]=(x_copy[column]>=low_redshift_upperbound) & (x_copy[column]<medium_redshift_upperbound)
        name="high_"+column
        x_copy[name]=x_copy[column]>=medium_redshift_upperbound
x_copy
Out[44]:
u g r i z redshift dim_u medium_u bright_u dim_g ... bright_r dim_i medium_i bright_i dim_z medium_z bright_z low_redshift medium_redshift high_redshift
0 19.47406 17.04240 15.94699 15.50342 15.22531 -0.000009 False True False False ... True False False True False False True True False False
1 18.66280 17.21449 16.67637 16.48922 16.39150 -0.000055 False True False False ... True False False True False False True True False False
2 19.38298 18.19169 17.47428 17.08732 16.80125 0.123111 False True False False ... False False True False False False True False True False
3 17.76536 16.60272 16.16116 15.98233 15.90438 -0.000111 False True False False ... True False False True False False True True False False
4 17.55025 16.26342 16.43869 16.55492 16.61326 0.000590 False True False False ... True False False True False False True True False False
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
309991 19.39861 18.35476 18.00348 17.89408 17.81222 -0.000101 False True False False ... False False True False False True False True False False
309992 19.07703 18.05159 17.78332 17.68976 17.66209 -0.000352 False True False False ... False False True False False True False True False False
309993 19.07982 17.51349 16.64037 16.24183 15.91180 0.117501 False True False False ... True False False True False False True False True False
309994 17.27528 16.41704 16.11662 15.98858 15.97745 -0.000400 False True False False ... True False False True False False True True False False
309995 17.90598 16.86471 16.51673 16.35695 16.22508 0.014457 False True False False ... True False False True False False True True False False

309996 rows × 24 columns

Zmapujemy również rodzaj naszego obiektu.

In [45]:
df_encoded = df.copy()
x_copy['class'] = pd.DataFrame(y)
x_copy.drop(features,axis=1,inplace=True)

x_copy['is_Star']=x_copy['class']=='STAR'
x_copy['is_Galaxy']=x_copy['class']=='GALAXY'
x_copy['is_QSO']=x_copy['class']=='QSO'
x_copy.drop('class',axis=1,inplace=True)

Następnie odnajdziemy reguły wskazujące na rodzaj naszego obiektu.

In [46]:
from mlxtend.frequent_patterns import apriori, association_rules

# Używamy apriori do znalezienia częstych zbiorów
frequent_itemsets = apriori(x_copy, min_support=0.05, use_colnames=True)

# Generowanie reguł asocjacyjnych
rules = association_rules(frequent_itemsets, metric="confidence", min_threshold=0.05,num_itemsets=len(x_copy))

# Filtrujemy reguły, które odnoszą się do kolumny 'class'
rules['consequents'] = rules['consequents'].apply(lambda x: list(x)[0])  # Zamieniamy krotki na elementy
star_rules=rules[rules['consequents']=='is_Star']
galaxy_rules =rules[rules['consequents']=='is_Galaxy']
qso_rules =rules[rules['consequents']=='is_QSO']
In [47]:
star_rules
Out[47]:
antecedents consequents antecedent support consequent support support confidence lift representativity leverage conviction zhangs_metric jaccard certainty kulczynski
34 (medium_u) is_Star 0.790971 0.326043 0.276097 0.349061 1.070598 1.0 0.018206 1.035361 0.315470 0.328328 0.034153 0.597936
68 (medium_g) is_Star 0.694667 0.326043 0.206370 0.297078 0.911163 1.0 -0.020121 0.958794 -0.242033 0.253421 -0.042977 0.465016
84 (bright_g) is_Star 0.214100 0.326043 0.111150 0.519150 1.592276 1.0 0.041344 1.401596 0.473302 0.259095 0.286528 0.430028
102 (medium_r) is_Star 0.584391 0.326043 0.168592 0.288492 0.884830 1.0 -0.021944 0.947224 -0.238491 0.227262 -0.055716 0.402790
116 (bright_r) is_Star 0.396483 0.326043 0.154854 0.390569 1.197905 1.0 0.025583 1.105878 0.273744 0.272787 0.095741 0.432759
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
15044 (bright_i, bright_g) is_Star 0.212190 0.130382 0.085766 0.404193 3.100059 1.0 0.058100 1.459562 0.859884 0.333970 0.314863 0.530997
15045 (bright_i, bright_r) is_Star 0.395179 0.086040 0.085766 0.217030 2.522433 1.0 0.051764 1.167299 0.997911 0.216879 0.143321 0.606921
15046 (bright_i, medium_u) is_Star 0.444712 0.109066 0.085766 0.192856 1.768256 1.0 0.037263 1.103811 0.782425 0.183255 0.094048 0.489611
15047 (bright_i, bright_z) is_Star 0.480429 0.086546 0.085766 0.178519 2.062699 1.0 0.044186 1.111960 0.991584 0.178229 0.100687 0.584749
15063 (bright_i) is_Star 0.481693 0.085920 0.085766 0.178050 2.072268 1.0 0.044378 1.112087 0.998322 0.177993 0.100790 0.588124

237 rows × 14 columns

In [48]:
galaxy_rules
Out[48]:
antecedents consequents antecedent support consequent support support confidence lift representativity leverage conviction zhangs_metric jaccard certainty kulczynski
13 (dim_u) is_Galaxy 0.171425 0.542288 0.125508 0.732147 1.350107 1.0 0.032547 1.708816 0.312969 0.213375 0.414800 0.481794
36 (medium_u) is_Galaxy 0.790971 0.542288 0.404347 0.511203 0.942679 1.0 -0.024587 0.936406 -0.225347 0.435291 -0.067913 0.628418
49 (dim_g) is_Galaxy 0.091233 0.542288 0.074553 0.817163 1.506881 1.0 0.025078 2.503388 0.370147 0.133375 0.600541 0.477320
70 (medium_g) is_Galaxy 0.694667 0.542288 0.366821 0.528053 0.973750 1.0 -0.009888 0.969838 -0.081126 0.421568 -0.031100 0.602243
86 (bright_g) is_Galaxy 0.214100 0.542288 0.100914 0.471342 0.869174 1.0 -0.015189 0.865801 -0.160737 0.153956 -0.154999 0.328716
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
14825 (bright_i, medium_redshift, medium_g, bright_r... is_Galaxy 0.118827 0.343411 0.117302 0.987159 2.874572 1.0 0.076495 51.133439 0.740062 0.340067 0.980443 0.664369
14829 (bright_i, medium_redshift, medium_g, medium_u... is_Galaxy 0.179244 0.237935 0.117302 0.654423 2.750423 1.0 0.074653 2.205193 0.775407 0.391164 0.546525 0.573710
15071 (bright_i, medium_redshift, bright_g, bright_r... is_Galaxy 0.051030 0.542288 0.050110 0.981984 1.810817 1.0 0.022438 25.405445 0.471841 0.092249 0.960638 0.537195
15077 (bright_i, medium_redshift, bright_g, bright_r... is_Galaxy 0.051033 0.343411 0.050110 0.981922 2.859320 1.0 0.032585 36.319020 0.685236 0.145528 0.972466 0.563921
15081 (bright_i, medium_redshift, bright_g, medium_u... is_Galaxy 0.051036 0.237935 0.050110 0.981860 4.126582 1.0 0.037967 42.009147 0.798417 0.209789 0.976196 0.596232

558 rows × 14 columns

In [49]:
qso_rules
Out[49]:
antecedents consequents antecedent support consequent support support confidence lift representativity leverage conviction zhangs_metric jaccard certainty kulczynski
38 (medium_u) is_QSO 0.790971 0.131669 0.110527 0.139736 1.061264 1.0 0.006380 1.009377 0.276170 0.136098 0.009290 0.489583
72 (medium_g) is_QSO 0.694667 0.131669 0.121476 0.174869 1.328091 1.0 0.030009 1.052355 0.809083 0.172340 0.049750 0.548725
106 (medium_r) is_QSO 0.584391 0.131669 0.124779 0.213520 1.621634 1.0 0.047833 1.104071 0.922353 0.211031 0.094262 0.580594
132 (medium_i) is_QSO 0.512784 0.131669 0.124666 0.243116 1.846414 1.0 0.057148 1.147244 0.940875 0.239841 0.128346 0.594964
154 (medium_z) is_QSO 0.465412 0.131669 0.123369 0.265075 2.013187 1.0 0.062089 1.181523 0.941427 0.260431 0.153635 0.601019
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
13196 (medium_redshift, medium_z, medium_u, medium_i... is_QSO 0.116566 0.131669 0.059823 0.513214 3.897748 1.0 0.044475 1.783805 0.841536 0.317513 0.439401 0.483780
13692 (medium_redshift, medium_z, medium_g, medium_i... is_QSO 0.169031 0.131669 0.062785 0.371438 2.820992 1.0 0.040528 1.381457 0.776822 0.263894 0.276126 0.424137
13754 (medium_z, high_redshift, medium_g, medium_i, ... is_QSO 0.053007 0.131669 0.052917 0.998296 7.581835 1.0 0.045937 509.585991 0.916697 0.401616 0.998038 0.700094
14569 (medium_redshift, medium_z, medium_g, medium_u... is_QSO 0.115395 0.131669 0.059520 0.515794 3.917344 1.0 0.044326 1.793310 0.841873 0.317366 0.442372 0.483918
14664 (medium_redshift, medium_z) is_QSO 0.244571 0.105324 0.059520 0.243366 2.310638 1.0 0.033761 1.182442 0.750857 0.204977 0.154292 0.404240

82 rows × 14 columns

Dla każdego zestawu odnajdziemy najwyższe możliwe wartości pewności ('confidence') - które powie nam w jakiej części wypadków gdy wystąpi dany zestaw cech to analizowany obiekt jest danego typu.

In [50]:
print(star_rules.loc[star_rules['confidence'].idxmax()]['antecedents'])
star_rules['confidence'].max()
frozenset({'medium_i', 'medium_z', 'low_redshift'})
Out[50]:
np.float64(0.8935126330843796)
In [51]:
print(galaxy_rules.loc[galaxy_rules['confidence'].idxmax()]['antecedents'])
galaxy_rules['confidence'].max()
frozenset({'bright_i', 'medium_redshift', 'medium_g', 'bright_r', 'bright_z'})
Out[51]:
np.float64(0.9875383487633371)
In [52]:
print(qso_rules.loc[qso_rules['confidence'].idxmax()]['antecedents'])
qso_rules['confidence'].max()
frozenset({'medium_z', 'high_redshift', 'medium_g', 'medium_i', 'medium_r'})
Out[52]:
np.float64(0.9982960077896786)

Porównując dane zauważyć możemy pewne charakterystyczne cechy np.

  • Wyraźny rozdział widoczny jest w przesunięciu ku czerwieni. Gwiazdy znajdujące się najbliżej mają niską wartość, galaktyki średnią, a obiekty QSO wysoką. Ma to sens z fizycznego/astonomicznego punktu widzenia - obiekty znajdujące się dalej mają bowiem wyższa wartość przesunięcia.
  • Galaktyki wskazywany są dodatkowo przez wysokie wartości światła w okolicy czerwieni/podczerwieni. Jest to interesująca obserwacja, galaktyki są często obserwowane w tej części spektrum, jako iż dobrze przenika on przez pył międzygwiezdny, ale jednocześniej nie jest do końca jasne dlaczego najjaśniejsze obiekty powinny być klasyfikowane jako galaktyki, a nie gwiazdy.

Nasze reguły okazały się najmniej skuteczne dla gwiazd, aby zrozumieć dlaczego posłużymy się jednak dalszą analizą - sprawdźmy jakie obiekty sa fałszywie wskazywane.

In [53]:
filtered_rows = x_copy[(x_copy['medium_z']) & (x_copy['low_redshift']) & (x_copy['medium_i']) & (~x_copy['is_Star']) ]
In [54]:
filtered_rows
Out[54]:
dim_u medium_u bright_u dim_g medium_g bright_g dim_r medium_r bright_r dim_i ... bright_i dim_z medium_z bright_z low_redshift medium_redshift high_redshift is_Star is_Galaxy is_QSO
9 False True False False True False False True False False ... False False True False True False False False True False
51 False True False False True False False True False False ... False False True False True False False False True False
76 False True False False True False False True False False ... False False True False True False False False True False
78 False True False False True False False True False False ... False False True False True False False False True False
121 False True False False True False False True False False ... False False True False True False False False True False
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
309568 False True False False True False False True False False ... False False True False True False False False True False
309609 False True False False True False False True False False ... False False True False True False False False True False
309763 False True False False True False False True False False ... False False True False True False False False True False
309872 False True False False True False False True False False ... False False True False True False False False True False
309942 False True False False True False False True False False ... False False True False True False False False True False

5361 rows × 21 columns

Wysnujemy hipotezę, iż fałszywa klasyfikacja wywołana jest tym, iż niektóre galaktyki znajdowały się na tyle blisko, iż zakwalifikowane zostały jako gwiazdy. Sprawdzimy to poprzez zmianę zakresów przesunięcia ku czerwieni na:

  • $x<0.01 $, niskie
  • $0.01<=x<1.5$, średnie
  • $ 2<=x$, wysokie

I powtórzenie operacji

In [55]:
x_copy = x.copy()
medium_upperbound = 22
bright_upperbound = 17
low_redshift_upperbound = 0.01
medium_redshift_upperbound = 1.5

for column in x_copy:   
    if column!="redshift":
        name = "dim_"+column
        x_copy[name] = x_copy[column] > medium_upperbound
        name = "medium_"+column
        x_copy[name] = (x_copy[column] <= medium_upperbound) &   (x_copy[column] > bright_upperbound)
        name= "bright_"+column
        x_copy[name] =x_copy[column] <= bright_upperbound
    else:
        name="low_"+column
        x_copy[name]=x_copy[column]<low_redshift_upperbound
        name="medium_"+column
        x_copy[name]=(x_copy[column]>=low_redshift_upperbound) & (x_copy[column]<medium_redshift_upperbound)
        name="high_"+column
        x_copy[name]=x_copy[column]>=medium_redshift_upperbound
df_encoded = df.copy()
x_copy['class'] = pd.DataFrame(y)
x_copy.drop(features,axis=1,inplace=True)

x_copy['is_Star']=x_copy['class']=='STAR'
x_copy['is_Galaxy']=x_copy['class']=='GALAXY'
x_copy['is_QSO']=x_copy['class']=='QSO'
x_copy.drop('class',axis=1,inplace=True)


# Używamy apriori do znalezienia częstych zbiorów
frequent_itemsets = apriori(x_copy, min_support=0.05, use_colnames=True)

# Generowanie reguł asocjacyjnych
rules = association_rules(frequent_itemsets, metric="confidence", min_threshold=0.05,num_itemsets=len(x_copy))

# Filtrujemy reguły, które odnoszą się do kolumny 'class'
rules['consequents'] = rules['consequents'].apply(lambda x: list(x)[0])  # Zamieniamy krotki na elementy
star_rules=rules[rules['consequents']=='is_Star']
galaxy_rules =rules[rules['consequents']=='is_Galaxy']
qso_rules =rules[rules['consequents']=='is_QSO']

print(star_rules.loc[star_rules['confidence'].idxmax()]['antecedents'])
star_rules['confidence'].max()
frozenset({'medium_z', 'low_redshift', 'medium_g', 'medium_u', 'medium_i'})
Out[55]:
np.float64(0.9839467708718381)
In [56]:
print(galaxy_rules.loc[galaxy_rules['confidence'].idxmax()]['antecedents'])
galaxy_rules['confidence'].max()
frozenset({'bright_i', 'medium_redshift', 'medium_g', 'bright_r', 'bright_z'})
Out[56]:
np.float64(0.9890944909589813)
In [57]:
print(qso_rules.loc[qso_rules['confidence'].idxmax()]['antecedents'])
qso_rules['confidence'].max()
frozenset({'medium_z', 'high_redshift', 'medium_g', 'medium_i', 'medium_r'})
Out[57]:
np.float64(0.9982960077896786)

Skuteczność naszej reguły w rozpoznawaniu gwiazd znacznie się zwiększyła - poprawa zakresów z pewnością zadziała. Wpływ przesunięcia ku czerwini na skuteczność reguł zdaje się być niezwykle wysoki. W dalszym ciągu galaktyki zdają się być jasniejsze w czerwieniach niż gwiazdy. Na koniec sprawdzimy odpowiedzi na dwa pytania wynikające z naszych obserwacji:

  • Czy przesunięcie ku czerwieni wystarczy dla określania rodzaju obiektu
  • Czy jasne w czerwieni obiekty to galaktyki?

Zacznijmy od przesunięcia ku czerwieni:

In [58]:
x_copy['class']=pd.DataFrame(y)
x_copy['low_redshift'] = x_copy['low_redshift'].astype(int)
x_copy['medium_redshift'] = x_copy['medium_redshift'].astype(int)
x_copy['high_redshift'] = x_copy['high_redshift'].astype(int)
x_copy.loc[x_copy['low_redshift'] == 1, 'redshift_category'] = 'Low'
x_copy.loc[x_copy['medium_redshift'] == 1, 'redshift_category'] = 'Medium'
x_copy.loc[x_copy['high_redshift'] == 1, 'redshift_category'] = 'High'


contingency_table = pd.crosstab(x_copy['class'], x_copy['redshift_category'])
contingency_percentage =  contingency_table.div(contingency_table.sum(axis=1), axis=0) * 100
plt.figure(figsize=(8, 6))
sns.heatmap(contingency_percentage, annot=True, linewidths=0.5, fmt='f')
plt.title("Contingency Table Heatmap: Class vs Redshift Categories")
plt.ylabel("Class")
plt.xlabel("Redshift Categories")
plt.show()
No description has been provided for this image

Wskazać możemy, iż niskie przesunięcie ku czerwieni praktycznie jednoznacznie wksazuje na gwiazdy, a wysokie na QSO. Sprawa nie jest jednak oczywista dla średniego przesunięcia. Nie popełnilibyśmy dużego błędu mówiąc, że wskazuje ono na galaktykę, ale chcą odróżnić galaktyki od QSO wynik byłby znaczący, ta obserwacja jest jednak również pokrywająca się ze współczesną astronomią - QSO to zasadniczo odmiana galaktyk.

Przeanalizujmy teraz czerwienie:

In [59]:
x_copy['dim_r'] = x_copy['dim_r'].astype(int)
x_copy['medium_r'] = x_copy['medium_r'].astype(int)
x_copy['bright_r'] = x_copy['bright_r'].astype(int)
x_copy.loc[x_copy['dim_r'] == 1, 'r_category'] = 'Dim'
x_copy.loc[x_copy['medium_r'] == 1, 'r_category'] = 'Medium'
x_copy.loc[x_copy['bright_r'] == 1, 'r_category'] = 'Bright'

x_copy['dim_i'] = x_copy['dim_i'].astype(int)
x_copy['medium_i'] = x_copy['medium_i'].astype(int)
x_copy['bright_i'] = x_copy['bright_i'].astype(int)
x_copy.loc[x_copy['dim_i'] == 1, 'i_category'] = 'Dim'
x_copy.loc[x_copy['medium_i'] == 1, 'i_category'] = 'Medium'
x_copy.loc[x_copy['bright_i'] == 1, 'i_category'] = 'Bright'

x_copy['dim_z'] = x_copy['dim_z'].astype(int)
x_copy['medium_z'] = x_copy['medium_z'].astype(int)
x_copy['bright_z'] = x_copy['bright_z'].astype(int)
x_copy.loc[x_copy['dim_z'] == 1, 'z_category'] = 'Dim'
x_copy.loc[x_copy['medium_z'] == 1, 'z_category'] = 'Medium'
x_copy.loc[x_copy['bright_z'] == 1, 'z_category'] = 'Bright'

contingency_table_r = pd.crosstab(x_copy['class'], x_copy['r_category'])
contingency_percentage_r =  contingency_table_r.div(contingency_table.sum(axis=1), axis=0) * 100
contingency_table_i = pd.crosstab(x_copy['class'], x_copy['i_category'])
contingency_percentage_i =  contingency_table_i.div(contingency_table.sum(axis=1), axis=0) * 100
contingency_table_z = pd.crosstab(x_copy['class'], x_copy['z_category'])
contingency_percentage_z =  contingency_table_z.div(contingency_table.sum(axis=1), axis=0) * 100

plt.figure(figsize=(12,10))
plt.subplot(2,2,1)
sns.heatmap(contingency_percentage_r, annot=True, linewidths=0.5, fmt='f')
plt.title("Class vs Red Light Categories")
plt.ylabel("Class")
plt.xlabel("Red Light Categories")

plt.subplot(2,2,2)
sns.heatmap(contingency_percentage_i, annot=True, linewidths=0.5, fmt='f')
plt.title("Class vs Near Infrared Categories")
plt.ylabel("Class")
plt.xlabel("Near Infrared Categories")

plt.subplot(2,2,3)
sns.heatmap(contingency_percentage_z, annot=True, linewidths=0.5, fmt='f')
plt.title("Class vs Infrared Categories")
plt.ylabel("Class")
plt.xlabel("Infrared Categories")
plt.show()
No description has been provided for this image

Zauważyć możemy, że obiekty o średniej jasności znajdują się praktycznie w każdej z grup, choć obiekty QSO najczęściej znajdują się w tej kategorii jasności, co znalazło odzwirciedlenie we wcześniej stworzonych regułach.

Obiekty jasne rozkąłdają się równomiernie między galaktyk i gwiazdy natomiast blade są praktycznie nie widoczne - wskazywać może to na miejsce do poprawy jako iż kategoria 'Dim' zdaje sie nie być użyteczna, a mogłaby posłużyć do lepszego objasnienia pozostałych. Podczerwienie i bliska podczerwień wskazują jedynie nieznacznie w stronę galaktyk - ten element utworzonych reguł jest albo typowy dla naszego zbioru, albo jego powiązanie z innymi cechami wskauzje na galaktyki, sam z siebie jest jednak niewystarczający.